// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <vector>

#include "base/strings/string_util.h"
#include "content/browser/frame_host/frame_tree_node.h"
#include "content/browser/frame_host/render_frame_host_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/host_zoom_map.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/notification_service.h"
#include "content/public/browser/notification_types.h"
#include "content/public/common/page_zoom.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/shell/browser/shell.h"
#include "content/test/content_browser_test_utils_internal.h"
#include "net/dns/mock_host_resolver.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"

namespace content {

// This class contains basic tests of zoom functionality.
class ZoomBrowserTest : public ContentBrowserTest {
public:
    ZoomBrowserTest() { }

protected:
    void SetUpOnMainThread() override
    {
        host_resolver()->AddRule("*", "127.0.0.1");
        SetupCrossSiteRedirector(embedded_test_server());
        ASSERT_TRUE(embedded_test_server()->Start());
    }

    WebContentsImpl* web_contents()
    {
        return static_cast<WebContentsImpl*>(shell()->web_contents());
    }
};

// This class contains tests to make sure that subframes zoom in a manner
// consistent with the top-level frame, even when the subframes are cross-site.
// Particular things we want to make sure of:
//
// * Subframes should always have the same zoom level as their main frame, even
// if the subframe's domain has a different zoom level stored in HostZoomMap.
//
// * The condition above should continue to hold after a navigation of the
// subframe.
//
// * Zoom changes applied to the mainframe should propagate to all subframes,
// regardless of whether they are same site or cross-site to the frame they are
// children of.
//
// The tests in this file rely on the notion that, when a page zooms, that
// subframes have both (1) a change in their frame rect, and (2) a change in
// their frame's scale. Since the page should scale as a unit, this means the
// innerWidth value of any subframe should be the same before and after the
// zoom (though it may transiently take on a different value). The
// FrameSizeObserver serves to watch for onresize events, and observes when
// the innerWidth is correctly set.
class IFrameZoomBrowserTest : public ContentBrowserTest {
public:
    IFrameZoomBrowserTest() { }

protected:
    void SetUpOnMainThread() override
    {
        host_resolver()->AddRule("*", "127.0.0.1");
        SetupCrossSiteRedirector(embedded_test_server());
        ASSERT_TRUE(embedded_test_server()->Start());
    }

    WebContentsImpl* web_contents()
    {
        return static_cast<WebContentsImpl*>(shell()->web_contents());
    }
};

namespace {

    const double kTolerance = 0.1; // In CSS pixels.

    double GetMainframeWindowBorder(const ToRenderFrameHost& adapter)
    {
        double border;
        const char kGetMainframeBorder[] = "window.domAutomationController.send("
                                           "window.outerWidth - window.innerWidth"
                                           ");";
        EXPECT_TRUE(
            ExecuteScriptAndExtractDouble(adapter, kGetMainframeBorder, &border));
        return border;
    }

    double GetMainFrameZoomFactor(const ToRenderFrameHost& adapter, double border)
    {
        const char kGetMainFrameZoomLevel[] = "window.domAutomationController.send("
                                              "(window.outerWidth - %f)/window.innerWidth"
                                              ");";
        double zoom_factor;
        EXPECT_TRUE(ExecuteScriptAndExtractDouble(
            adapter, base::StringPrintf(kGetMainFrameZoomLevel, border),
            &zoom_factor));
        return zoom_factor;
    }

    double GetSubframeWidth(const ToRenderFrameHost& adapter)
    {
        double width;
        EXPECT_TRUE(ExecuteScriptAndExtractDouble(
            adapter, "window.domAutomationController.send(window.innerWidth);",
            &width));
        return width;
    }

    // This struct is used to track changes to subframes after a main frame zoom
    // change, so that we can test subframe inner widths with assurance that all the
    // changes have finished propagating.
    struct FrameResizeObserver {
        FrameResizeObserver(RenderFrameHost* host,
            std::string label,
            double inner_width,
            double tolerance)
            : frame_host(host)
            , msg_label(std::move(label))
            , zoomed_correctly(false)
            , expected_inner_width(inner_width)
            , tolerance(tolerance)
        {
            SetupOnResizeCallback(host, msg_label);
        }

        void SetupOnResizeCallback(const ToRenderFrameHost& adapter,
            const std::string& label)
        {
            const char kOnResizeCallbackSetup[] = "document.body.onresize = function(){"
                                                  "  window.domAutomationController.setAutomationId(0);"
                                                  "  window.domAutomationController.send('%s ' + window.innerWidth);"
                                                  "};";
            EXPECT_TRUE(ExecuteScript(
                adapter, base::StringPrintf(kOnResizeCallbackSetup, label.c_str())));
        }

        void Check(const std::string& status_msg)
        {
            if (!base::StartsWith(status_msg, msg_label, base::CompareCase::SENSITIVE))
                return;

            double inner_width = std::stod(status_msg.substr(msg_label.length() + 1));
            zoomed_correctly = std::abs(expected_inner_width - inner_width) < tolerance;
        }

        FrameResizeObserver* toThis() { return this; }

        RenderFrameHost* frame_host;
        std::string msg_label;
        bool zoomed_correctly;
        double expected_inner_width;
        double tolerance;
    };

    // This struct is used to wait until a resize has occurred.
    struct ResizeObserver {
        ResizeObserver(RenderFrameHost* host)
            : frame_host(host)
        {
            SetupOnResizeCallback(host);
        }

        void SetupOnResizeCallback(const ToRenderFrameHost& adapter)
        {
            const char kOnResizeCallbackSetup[] = "document.body.onresize = function(){"
                                                  "  window.domAutomationController.setAutomationId(0);"
                                                  "  window.domAutomationController.send('Resized');"
                                                  "};";
            EXPECT_TRUE(ExecuteScript(
                adapter, kOnResizeCallbackSetup));
        }

        bool IsResizeCallback(const std::string& status_msg)
        {
            return status_msg == "Resized";
        }

        RenderFrameHost* frame_host;
    };

    void WaitForResize(DOMMessageQueue& msg_queue, ResizeObserver& observer)
    {
        std::string status;
        while (msg_queue.WaitForMessage(&status)) {
            // Strip the double quotes from the message.
            status = status.substr(1, status.length() - 2);
            if (observer.IsResizeCallback(status))
                break;
        }
    }

    void WaitAndCheckFrameZoom(
        DOMMessageQueue& msg_queue,
        std::vector<FrameResizeObserver>& frame_observers)
    {
        std::string status;
        while (msg_queue.WaitForMessage(&status)) {
            // Strip the double quotes from the message.
            status = status.substr(1, status.length() - 2);

            bool all_zoomed_correctly = true;

            // Use auto& to operate on a reference, and not a copy.
            for (auto& observer : frame_observers) {
                observer.Check(status);
                all_zoomed_correctly = all_zoomed_correctly && observer.zoomed_correctly;
            }

            if (all_zoomed_correctly)
                break;
        }
    }

} // namespace

IN_PROC_BROWSER_TEST_F(ZoomBrowserTest, ZoomPreservedOnReload)
{
    std::string top_level_host("a.com");

    GURL main_url(embedded_test_server()->GetURL(
        top_level_host, "/cross_site_iframe_factory.html?a(b(a))"));
    EXPECT_TRUE(NavigateToURL(shell(), main_url));
    NavigationEntry* entry = web_contents()->GetController().GetLastCommittedEntry();
    ASSERT_TRUE(entry);
    GURL loaded_url = HostZoomMap::GetURLFromEntry(entry);
    EXPECT_EQ(top_level_host, loaded_url.host());

    FrameTreeNode* root = static_cast<WebContentsImpl*>(web_contents())->GetFrameTree()->root();
    double main_frame_window_border = GetMainframeWindowBorder(web_contents());

    HostZoomMap* host_zoom_map = HostZoomMap::GetForWebContents(web_contents());
    double default_zoom_level = host_zoom_map->GetDefaultZoomLevel();
    EXPECT_EQ(0.0, default_zoom_level);

    EXPECT_DOUBLE_EQ(
        1.0, GetMainFrameZoomFactor(web_contents(), main_frame_window_border));

    const double new_zoom_factor = 2.5;

    // Set the new zoom, wait for the page to be resized, and sanity-check that
    // the zoom was applied.
    {
        DOMMessageQueue msg_queue;
        ResizeObserver observer(root->current_frame_host());

        const double new_zoom_level = default_zoom_level + ZoomFactorToZoomLevel(new_zoom_factor);
        host_zoom_map->SetZoomLevelForHost(top_level_host, new_zoom_level);

        WaitForResize(msg_queue, observer);
    }

    // Make this comparison approximate for Nexus5X test;
    // https://crbug.com/622858.
    EXPECT_NEAR(
        new_zoom_factor,
        GetMainFrameZoomFactor(web_contents(), main_frame_window_border),
        0.01);

    // Now the actual test: Reload the page and check that the main frame is
    // still properly zoomed.
    WindowedNotificationObserver load_stop_observer(
        NOTIFICATION_LOAD_STOP,
        NotificationService::AllSources());
    shell()->Reload();
    load_stop_observer.Wait();

    EXPECT_NEAR(
        new_zoom_factor,
        GetMainFrameZoomFactor(web_contents(), main_frame_window_border),
        0.01);
}

IN_PROC_BROWSER_TEST_F(IFrameZoomBrowserTest, SubframesZoomProperly)
{
    std::string top_level_host("a.com");
    GURL main_url(embedded_test_server()->GetURL(
        top_level_host, "/cross_site_iframe_factory.html?a(b(a))"));
    EXPECT_TRUE(NavigateToURL(shell(), main_url));
    NavigationEntry* entry = web_contents()->GetController().GetLastCommittedEntry();
    ASSERT_TRUE(entry);
    GURL loaded_url = HostZoomMap::GetURLFromEntry(entry);
    EXPECT_EQ(top_level_host, loaded_url.host());

    FrameTreeNode* root = static_cast<WebContentsImpl*>(web_contents())->GetFrameTree()->root();
    RenderFrameHostImpl* child = root->child_at(0)->current_frame_host();
    RenderFrameHostImpl* grandchild = root->child_at(0)->child_at(0)->current_frame_host();

    // The following calls must be made when the page's scale factor = 1.0.
    double scale_one_child_width = GetSubframeWidth(child);
    double scale_one_grandchild_width = GetSubframeWidth(grandchild);
    double main_frame_window_border = GetMainframeWindowBorder(web_contents());

    HostZoomMap* host_zoom_map = HostZoomMap::GetForWebContents(web_contents());
    double default_zoom_level = host_zoom_map->GetDefaultZoomLevel();
    EXPECT_EQ(0.0, default_zoom_level);

    EXPECT_DOUBLE_EQ(
        1.0, GetMainFrameZoomFactor(web_contents(), main_frame_window_border));

    const double new_zoom_factor = 2.5;
    {
        DOMMessageQueue msg_queue;

        std::vector<FrameResizeObserver> frame_observers;
        frame_observers.emplace_back(child, "child",
            scale_one_child_width, kTolerance);
        frame_observers.emplace_back(grandchild, "grandchild",
            scale_one_grandchild_width, kTolerance);

        const double new_zoom_level = default_zoom_level + ZoomFactorToZoomLevel(new_zoom_factor);
        host_zoom_map->SetZoomLevelForHost(top_level_host, new_zoom_level);

        WaitAndCheckFrameZoom(msg_queue, frame_observers);
    }

    // Make this comparison approximate for Nexus5X test;
    // https://crbug.com/622858.
    EXPECT_NEAR(
        new_zoom_factor,
        GetMainFrameZoomFactor(web_contents(), main_frame_window_border),
        0.01);
}

IN_PROC_BROWSER_TEST_F(IFrameZoomBrowserTest, SubframesDontZoomIndependently)
{
    std::string top_level_host("a.com");
    GURL main_url(embedded_test_server()->GetURL(
        top_level_host, "/cross_site_iframe_factory.html?a(b(a))"));
    EXPECT_TRUE(NavigateToURL(shell(), main_url));
    NavigationEntry* entry = web_contents()->GetController().GetLastCommittedEntry();
    ASSERT_TRUE(entry);
    GURL loaded_url = HostZoomMap::GetURLFromEntry(entry);
    EXPECT_EQ(top_level_host, loaded_url.host());

    FrameTreeNode* root = static_cast<WebContentsImpl*>(web_contents())->GetFrameTree()->root();
    RenderFrameHostImpl* child = root->child_at(0)->current_frame_host();
    RenderFrameHostImpl* grandchild = root->child_at(0)->child_at(0)->current_frame_host();

    // The following calls must be made when the page's scale factor = 1.0.
    double scale_one_child_width = GetSubframeWidth(child);
    double scale_one_grandchild_width = GetSubframeWidth(grandchild);
    double main_frame_window_border = GetMainframeWindowBorder(web_contents());

    HostZoomMap* host_zoom_map = HostZoomMap::GetForWebContents(web_contents());
    double default_zoom_level = host_zoom_map->GetDefaultZoomLevel();
    EXPECT_EQ(0.0, default_zoom_level);

    EXPECT_DOUBLE_EQ(
        1.0, GetMainFrameZoomFactor(web_contents(), main_frame_window_border));

    const double new_zoom_factor = 2.0;
    const double new_zoom_level = default_zoom_level + ZoomFactorToZoomLevel(new_zoom_factor);

    // This should not cause the nested iframe to change its zoom.
    host_zoom_map->SetZoomLevelForHost("b.com", new_zoom_level);

    EXPECT_DOUBLE_EQ(
        1.0, GetMainFrameZoomFactor(web_contents(), main_frame_window_border));
    EXPECT_EQ(scale_one_child_width, GetSubframeWidth(child));
    EXPECT_EQ(scale_one_grandchild_width, GetSubframeWidth(grandchild));

    // We exclude the remainder of this test on Android since Android does not
    // set page zoom levels for loading pages.
    // See RenderFrameImpl::SetHostZoomLevel().
#if !defined(OS_ANDROID)
    // When we navigate so that b.com is the top-level site, then it has the
    // expected zoom.
    GURL new_url = embedded_test_server()->GetURL("b.com", "/title1.html");
    EXPECT_TRUE(NavigateToURL(shell(), new_url));
    EXPECT_DOUBLE_EQ(
        new_zoom_factor,
        GetMainFrameZoomFactor(web_contents(), main_frame_window_border));
#endif
}

IN_PROC_BROWSER_TEST_F(IFrameZoomBrowserTest, AllFramesGetDefaultZoom)
{
    std::string top_level_host("a.com");
    GURL main_url(embedded_test_server()->GetURL(
        top_level_host, "/cross_site_iframe_factory.html?a(b(a))"));
    EXPECT_TRUE(NavigateToURL(shell(), main_url));
    NavigationEntry* entry = web_contents()->GetController().GetLastCommittedEntry();
    ASSERT_TRUE(entry);
    GURL loaded_url = HostZoomMap::GetURLFromEntry(entry);
    EXPECT_EQ(top_level_host, loaded_url.host());

    FrameTreeNode* root = static_cast<WebContentsImpl*>(web_contents())->GetFrameTree()->root();
    RenderFrameHostImpl* child = root->child_at(0)->current_frame_host();
    RenderFrameHostImpl* grandchild = root->child_at(0)->child_at(0)->current_frame_host();

    // The following calls must be made when the page's scale factor = 1.0.
    double scale_one_child_width = GetSubframeWidth(child);
    double scale_one_grandchild_width = GetSubframeWidth(grandchild);
    double main_frame_window_border = GetMainframeWindowBorder(web_contents());

    HostZoomMap* host_zoom_map = HostZoomMap::GetForWebContents(web_contents());
    double default_zoom_level = host_zoom_map->GetDefaultZoomLevel();
    EXPECT_EQ(0.0, default_zoom_level);

    EXPECT_DOUBLE_EQ(
        1.0, GetMainFrameZoomFactor(web_contents(), main_frame_window_border));

    const double new_default_zoom_factor = 2.0;
    {
        DOMMessageQueue msg_queue;

        std::vector<FrameResizeObserver> frame_observers;
        frame_observers.emplace_back(child, "child",
            scale_one_child_width, kTolerance);
        frame_observers.emplace_back(grandchild, "grandchild",
            scale_one_grandchild_width, kTolerance);

        const double new_default_zoom_level = default_zoom_level + ZoomFactorToZoomLevel(new_default_zoom_factor);

        host_zoom_map->SetZoomLevelForHost("b.com", new_default_zoom_level + 1.0);
        host_zoom_map->SetDefaultZoomLevel(new_default_zoom_level);

        WaitAndCheckFrameZoom(msg_queue, frame_observers);
    }
    // Make this comparison approximate for Nexus5X test;
    // https://crbug.com/622858.
    EXPECT_NEAR(
        new_default_zoom_factor,
        GetMainFrameZoomFactor(web_contents(), main_frame_window_border),
        0.01);
}

IN_PROC_BROWSER_TEST_F(IFrameZoomBrowserTest, SiblingFramesZoom)
{
    std::string top_level_host("a.com");
    GURL main_url(embedded_test_server()->GetURL(
        top_level_host, "/cross_site_iframe_factory.html?a(b,b)"));
    EXPECT_TRUE(NavigateToURL(shell(), main_url));
    NavigationEntry* entry = web_contents()->GetController().GetLastCommittedEntry();
    ASSERT_TRUE(entry);
    GURL loaded_url = HostZoomMap::GetURLFromEntry(entry);
    EXPECT_EQ(top_level_host, loaded_url.host());

    FrameTreeNode* root = static_cast<WebContentsImpl*>(web_contents())->GetFrameTree()->root();
    RenderFrameHostImpl* child1 = root->child_at(0)->current_frame_host();
    RenderFrameHostImpl* child2 = root->child_at(1)->current_frame_host();

    // The following calls must be made when the page's scale factor = 1.0.
    double scale_one_child1_width = GetSubframeWidth(child1);
    double scale_one_child2_width = GetSubframeWidth(child2);
    double main_frame_window_border = GetMainframeWindowBorder(web_contents());

    HostZoomMap* host_zoom_map = HostZoomMap::GetForWebContents(web_contents());
    double default_zoom_level = host_zoom_map->GetDefaultZoomLevel();
    EXPECT_EQ(0.0, default_zoom_level);

    EXPECT_DOUBLE_EQ(
        1.0, GetMainFrameZoomFactor(web_contents(), main_frame_window_border));

    const double new_zoom_factor = 2.5;
    {
        DOMMessageQueue msg_queue;

        std::vector<FrameResizeObserver> frame_observers;
        frame_observers.emplace_back(child1, "child1",
            scale_one_child1_width, kTolerance);
        frame_observers.emplace_back(child2, "child2",
            scale_one_child2_width, kTolerance);

        const double new_zoom_level = default_zoom_level + ZoomFactorToZoomLevel(new_zoom_factor);
        host_zoom_map->SetZoomLevelForHost(top_level_host, new_zoom_level);

        WaitAndCheckFrameZoom(msg_queue, frame_observers);
    }

    // Make this comparison approximate for Nexus5X test;
    // https://crbug.com/622858.
    EXPECT_NEAR(
        new_zoom_factor,
        GetMainFrameZoomFactor(web_contents(), main_frame_window_border),
        0.01);
}

IN_PROC_BROWSER_TEST_F(IFrameZoomBrowserTest, SubframeRetainsZoomOnNavigation)
{
    std::string top_level_host("a.com");
    GURL main_url(embedded_test_server()->GetURL(
        top_level_host, "/cross_site_iframe_factory.html?a(b)"));
    EXPECT_TRUE(NavigateToURL(shell(), main_url));
    NavigationEntry* entry = web_contents()->GetController().GetLastCommittedEntry();
    ASSERT_TRUE(entry);
    GURL loaded_url = HostZoomMap::GetURLFromEntry(entry);
    EXPECT_EQ(top_level_host, loaded_url.host());

    FrameTreeNode* root = static_cast<WebContentsImpl*>(web_contents())->GetFrameTree()->root();
    RenderFrameHostImpl* child = root->child_at(0)->current_frame_host();

    // The following calls must be made when the page's scale factor = 1.0.
    double scale_one_child_width = GetSubframeWidth(child);
    double main_frame_window_border = GetMainframeWindowBorder(web_contents());

    HostZoomMap* host_zoom_map = HostZoomMap::GetForWebContents(web_contents());
    double default_zoom_level = host_zoom_map->GetDefaultZoomLevel();
    EXPECT_EQ(0.0, default_zoom_level);

    EXPECT_DOUBLE_EQ(
        1.0, GetMainFrameZoomFactor(web_contents(), main_frame_window_border));

    const double new_zoom_factor = 0.5;
    {
        DOMMessageQueue msg_queue;

        std::vector<FrameResizeObserver> frame_observers;
        frame_observers.emplace_back(child, "child",
            scale_one_child_width, kTolerance);

        const double new_zoom_level = default_zoom_level + ZoomFactorToZoomLevel(new_zoom_factor);
        host_zoom_map->SetZoomLevelForHost(top_level_host, new_zoom_level);

        WaitAndCheckFrameZoom(msg_queue, frame_observers);
    }

    // Make this comparison approximate for Nexus5X test;
    // https://crbug.com/622858.
    EXPECT_NEAR(
        new_zoom_factor,
        GetMainFrameZoomFactor(web_contents(), main_frame_window_border),
        0.01);

    // Navigate child frame cross site, and make sure zoom is the same.
    TestNavigationObserver observer(web_contents());
    GURL url = embedded_test_server()->GetURL("c.com", "/title1.html");
    NavigateFrameToURL(root->child_at(0), url);
    EXPECT_TRUE(observer.last_navigation_succeeded());
    EXPECT_EQ(url, observer.last_navigation_url());

    // Check that the child frame maintained the same scale after navigating
    // cross-site.
    double new_child_width = GetSubframeWidth(root->child_at(0)->current_frame_host());
    EXPECT_EQ(scale_one_child_width, new_child_width);
}

// http://crbug.com/609213
#if !defined(OS_ANDROID)
IN_PROC_BROWSER_TEST_F(IFrameZoomBrowserTest,
    RedirectToPageWithSubframeZoomsCorrectly)
{
    std::string initial_host("a.com");
    std::string redirected_host("b.com");
    EXPECT_TRUE(NavigateToURL(shell(), GURL(embedded_test_server()->GetURL(initial_host, "/title2.html"))));
    double main_frame_window_border = GetMainframeWindowBorder(web_contents());
    EXPECT_DOUBLE_EQ(
        1.0, GetMainFrameZoomFactor(web_contents(), main_frame_window_border));

    // Set a zoom level for b.com before we navigate to it.
    const double kZoomFactorForRedirectedHost = 1.5;
    HostZoomMap* host_zoom_map = HostZoomMap::GetForWebContents(web_contents());
    host_zoom_map->SetZoomLevelForHost(
        redirected_host, ZoomFactorToZoomLevel(kZoomFactorForRedirectedHost));

    // Navigation to a.com doesn't change the zoom level, but when it redirects
    // to b.com, and then a subframe loads, the zoom should change.
    GURL redirect_url(embedded_test_server()->GetURL(
        redirected_host, "/cross_site_iframe_factory.html?b(b)"));
    GURL url(embedded_test_server()->GetURL(
        initial_host, "/client-redirect?" + redirect_url.spec()));

    NavigateToURLBlockUntilNavigationsComplete(shell(), url, 2);
    EXPECT_TRUE(IsLastCommittedEntryOfPageType(web_contents(), PAGE_TYPE_NORMAL));
    EXPECT_EQ(redirect_url, web_contents()->GetLastCommittedURL());

    EXPECT_NEAR(
        kZoomFactorForRedirectedHost,
        GetMainFrameZoomFactor(web_contents(), main_frame_window_border),
        0.001);
}
#endif

// Tests that on cross-site navigation from a page that has a subframe, the
// appropriate zoom is applied to the new page.
// crbug.com/673065
// Note: We exclude the this test on Android since Android does not set page
// zoom levels for loading pages.
// See RenderFrameImpl::SetHostZoomLevel().
#if !defined(OS_ANDROID)
IN_PROC_BROWSER_TEST_F(IFrameZoomBrowserTest,
    SubframesDontBreakConnectionToRenderer)
{
    std::string top_level_host("a.com");
    GURL main_url(embedded_test_server()->GetURL(
        top_level_host, "/page_with_iframe_and_link.html"));
    EXPECT_TRUE(NavigateToURL(shell(), main_url));
    NavigationEntry* entry = web_contents()->GetController().GetLastCommittedEntry();
    ASSERT_TRUE(entry);
    GURL loaded_url = HostZoomMap::GetURLFromEntry(entry);
    EXPECT_EQ(top_level_host, loaded_url.host());

    // The following calls must be made when the page's scale factor = 1.0.
    double main_frame_window_border = GetMainframeWindowBorder(web_contents());

    HostZoomMap* host_zoom_map = HostZoomMap::GetForWebContents(web_contents());
    double default_zoom_level = host_zoom_map->GetDefaultZoomLevel();
    EXPECT_EQ(0.0, default_zoom_level);
    EXPECT_DOUBLE_EQ(
        1.0, GetMainFrameZoomFactor(web_contents(), main_frame_window_border));

    // Set a zoom for a host that will be navigated to below.
    const double new_zoom_factor = 2.0;
    const double new_zoom_level = default_zoom_level + ZoomFactorToZoomLevel(new_zoom_factor);
    host_zoom_map->SetZoomLevelForHost("foo.com", new_zoom_level);

    // Navigate forward in the same RFH to a site with that host via a
    // renderer-initiated navigation.
    {
        const char kReplacePortNumber[] = "window.domAutomationController.send(setPortNumber(%d));";
        uint16_t port_number = embedded_test_server()->port();
        bool success = false;
        EXPECT_TRUE(ExecuteScriptAndExtractBool(
            shell(), base::StringPrintf(kReplacePortNumber, port_number),
            &success));
        TestNavigationObserver observer(shell()->web_contents());
        GURL url = embedded_test_server()->GetURL("foo.com", "/title2.html");
        success = false;
        EXPECT_TRUE(ExecuteScriptAndExtractBool(
            shell(), "window.domAutomationController.send(clickCrossSiteLink());",
            &success));
        EXPECT_TRUE(success);
        EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
        EXPECT_EQ(url, observer.last_navigation_url());
        EXPECT_TRUE(observer.last_navigation_succeeded());
    }

    // Check that the requested zoom has been applied to the new site.
    // NOTE: Local observation on Linux has shown that this comparison has to be
    // approximate. As the common failure mode would be that the zoom is ~1
    // instead of ~2, this approximation shouldn't be problematic.
    EXPECT_NEAR(
        new_zoom_factor,
        GetMainFrameZoomFactor(web_contents(), main_frame_window_border),
        .1);
}
#endif

} // namespace content
